﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Security;
using System.Xml;
using System.IO;
using System.IO.IsolatedStorage;
using log4net;

namespace Entangle
{
    class Cryptography :ICrypto
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Cryptography));

        #region Singleton
        private Cryptography()
        {
            log.Debug("Creating Cryptography instance.");
        }

        private static ICrypto _instance;
        public static ICrypto Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new Cryptography();
                }
                return _instance;
            }
        }
        public static ICrypto GetInstance()
        {
            if (_instance == null)
            {
                _instance = new Cryptography();
            }
            return _instance;
        }
        #endregion

        private RSACryptoServiceProvider rsa;
        private RSACryptoServiceProvider partnerRsa;

        private SymmetricKeyRing symmKeys;

        #region RSA Keys
        private void GenerateKeys()
        {
            log.Info("Generating RSA private key.");
            rsa = new RSACryptoServiceProvider(Properties.Settings.Default.PrivKeyLength);
            log.Info("RSA private key has been generated. It will be saved now.");
            SavePrivateKey();
        }

        private void SavePrivateKey()
        {
            log.Info("Saving private key...");

            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Writing to file: " + Properties.Settings.Default.PrivKeyFileName);
            StreamWriter writer = new StreamWriter(new IsolatedStorageFileStream(Properties.Settings.Default.PrivKeyFileName, FileMode.Create, isoStore));
            writer.Write(rsa.ToXmlString(true));
            writer.Close();

            log.Info("Private key saved.");
        }

        private void LoadPrivateKey()
        {
            log.Info("Loading private key...");
            if (rsa != null)
            {
                log.Info("Private key already loaded.");
                return;
            }
            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Checking for private key file.");
            string[] fileNames = isoStore.GetFileNames(Properties.Settings.Default.PrivKeyFileName);
            if (fileNames.Length == 0)
            {
                log.Info("Private key file not found, it will be generated.");
                GenerateKeys();
            }
            else
            {
                log.Debug("Reading from file: " + Properties.Settings.Default.PrivKeyFileName);
                StreamReader reader = new StreamReader(new IsolatedStorageFileStream(Properties.Settings.Default.PrivKeyFileName, FileMode.Open, isoStore));
                string privKeyXML = reader.ReadToEnd();

                log.Debug("Loading XML");
                RSACryptoServiceProvider tRSA = new RSACryptoServiceProvider();
                tRSA.FromXmlString(privKeyXML);

                log.Info("Private key loaded.");
                rsa = tRSA;
            }
        }

        private bool isPrivateKeyLoaded { get { return rsa != null; } }
        private bool isPartnerKeyLoaded { get { return partnerRsa != null; } }
        public string GetPublicKeyXML()
        {
            if (!isPrivateKeyLoaded)
                LoadPrivateKey();

            return rsa.ToXmlString(false);
        }

        private void SavePartnerPublicKey()
        {
            log.Info("Saving partner public key...");

            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Writing to file: " + Properties.Settings.Default.PartnerKeyFileName);
            StreamWriter writer = new StreamWriter(new IsolatedStorageFileStream(Properties.Settings.Default.PartnerKeyFileName, FileMode.Create, isoStore));
            writer.Write(partnerRsa.ToXmlString(false));
            writer.Close();

            log.Info("Partner public key saved.");
        }

        public bool LoadPartnerPublicKey()
        {
            log.Info("Loading partner public key...");
            if (Configuration.SynchStatus == SynchStatus.Unknown)
                throw new EntanglementException("Cannot load partner key, synch must be run first");

            if (partnerRsa != null)
            {
                log.Info("Partner key already loaded.");
                return true;
            }
            log.Debug("Obtaining isolated storage.");
            IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null);

            log.Debug("Checking for partner key file.");
            string[] fileNames = isoStore.GetFileNames(Properties.Settings.Default.PartnerKeyFileName);
            if (fileNames.Length == 0)
            {
                log.Info("Partner key file not found, it will be requested.");
                Message mKey = Entangled.Instance.RequestPublicKey();
                switch (mKey.Type)
                {
                    case MessageType.PUBLIC_KEY_SUBMIT:
                        //ok, continue
                        break;
                    case MessageType.PARTNER_KEY_FROM_SERV:
                        //ok, continue
                        break;
                    case MessageType.PARTNER_NOT_SYNCHED:
                        log.Info("Partner haven't synched yet.");
                        return false;
                    default:
                        throw new EntanglementException(String.Format("System error: requested message type: {0}, obtained: {1}", MessageType.PUBLIC_KEY_SUBMIT, mKey.Type));
                }
                string publicKeyXML = mKey.Payload;
                partnerRsa = new RSACryptoServiceProvider();
                try
                {
                    partnerRsa.FromXmlString(publicKeyXML);
                }
                catch (ArgumentNullException ane)
                {
                    log.Error("Partner public key is missing from message obtained from server.", ane);
                    throw new EntanglementException("Server provided invalid partner public key", ane);
                }
                catch (CryptographicException ce)
                {
                    log.Error("Partner public key in message obtained from server is not in valid format.", ce);
                    throw new EntanglementException("Server provided invalid partner public key", ce);
                }
                log.Info("Partner public key obtained from server.");
                SavePartnerPublicKey();
            }
            else
            {
                log.Debug("Reading from file: " + Properties.Settings.Default.PartnerKeyFileName);
                StreamReader reader = new StreamReader(new IsolatedStorageFileStream(Properties.Settings.Default.PartnerKeyFileName, FileMode.Open, isoStore));
                string partnerKeyXML = reader.ReadToEnd();

                log.Debug("Loading XML");
                RSACryptoServiceProvider tRSA = new RSACryptoServiceProvider();
                tRSA.FromXmlString(partnerKeyXML);

                log.Info("Partner public key loaded.");
                partnerRsa = tRSA;
            }
            return true;
        }

        public byte[] PublicKeyHash
        {
            get
            {
                SHA512 sha = new SHA512Managed();
                return sha.ComputeHash(rsa.ExportCspBlob(false));
            }
        }

        public byte[] PartnerPublicKeyHash
        {
            get
            {
                if (!isPartnerKeyLoaded)
                    LoadPartnerPublicKey();

                SHA512 sha = new SHA512Managed();
                return sha.ComputeHash(partnerRsa.ExportCspBlob(false));
            }
        }

        #endregion

        #region RSA Signing/Verification
        public string SignData(string data)
        {
            log.Debug("Signing data.");
            if (!isPrivateKeyLoaded)
                LoadPrivateKey();

            log.Debug("Hashing data");
            SHA512 sha = new SHA512Managed();
            byte[] hash = sha.ComputeHash(new UTF8Encoding().GetBytes(data));

            log.Debug("Signing hash with private key.");
            byte[] signed = rsa.SignHash(hash, CryptoConfig.MapNameToOID("SHA512"));

            log.Debug("Data signed succesfully.");
            return Convert.ToBase64String(signed);
        }
        public bool VerifyData(string data, string signature, string hashAlgorithmName)
        {
            log.Debug("Verifying data.");

            if (!isPartnerKeyLoaded)
                LoadPartnerPublicKey();

            log.Debug("Parse hash algorithm name");
            SupportedHashAlgorithms algorithmType = (SupportedHashAlgorithms)Enum.Parse(typeof(SupportedHashAlgorithms), hashAlgorithmName);
            if (!Enum.IsDefined(typeof(SupportedHashAlgorithms), algorithmType))
                throw new MessageException("Unsupported hashing algorithm: " + hashAlgorithmName);

            log.Debug("Create hashing algorithm");
            HashAlgorithm algorithm;
            switch (algorithmType)
            {
                case SupportedHashAlgorithms.SHA512:
                    algorithm = new SHA512Managed();
                    break;
                default:
                    throw new MessageException("Unsupported hashing algorithm: " + hashAlgorithmName);
            }
            log.Debug("Hashing data");
            byte[] hash = algorithm.ComputeHash(new UTF8Encoding().GetBytes(data));

            log.Debug("Verifing hash");
            bool result = partnerRsa.VerifyHash(hash, CryptoConfig.MapNameToOID(Enum.GetName(typeof(SupportedHashAlgorithms), algorithmType)), Convert.FromBase64String(signature));
            log.Debug("Verification result: " + (result ? "OK" : "FAILED"));

            return result;
        }
        #endregion

        #region RSA Encryption/Decryption
        public byte[] RSAEncryptWithPartnerKeyLongData(byte[] data)
        {
            log.Debug("Begin long data RSA Encryption (PartnerKey)");
            log.DebugFormat("Data to encrypt: '{0}'", Convert.ToBase64String(data));
            
            int blockSize = (partnerRsa.KeySize - 384) / 8;
            int blockNo = (int) Math.Ceiling((double)(data.Length / blockSize));
            log.DebugFormat("blockSize={0}, blockNo={1}", blockSize, blockNo);

            byte[] r = new byte[blockSize * blockNo];
            for (int i = 0; i < blockNo; ++i)
            {
                byte[] plain = new byte[blockSize];
                Array.Copy(data, i * blockSize, plain, 0, blockSize);
                byte[] enc = RSAEncryptWithPartnerKey(plain);
                Array.Copy(enc, 0, r, i * blockSize, blockSize);
                log.DebugFormat("[Run {0}]: plain='{1}',enc='{2}'", i, Convert.ToBase64String(plain), Convert.ToBase64String(enc));
            }
            log.DebugFormat("Encrypted data: '{0}'", Convert.ToBase64String(r));
            return r;
        }
        public byte[] RSAEncryptWithPrivateKeyLongData(byte[] data)
        {
            log.Debug("Begin long data RSA Encryption (PrivateKey)");
            log.DebugFormat("Data to encrypt: '{0}'", Convert.ToBase64String(data));
            
            int blockSize = (rsa.KeySize - 384) / 8;
            int blockNo = (int)Math.Ceiling((double)(data.Length / blockSize));
            log.DebugFormat("blockSize={0}, blockNo={1}", blockSize, blockNo);

            byte[] r = new byte[blockSize * blockNo];
            for (int i = 0; i < blockNo; ++i)
            {
                byte[] plain = new byte[blockSize];
                Array.Copy(data, i * blockSize, plain, 0, blockSize);
                byte[] enc = RSAEncryptWithPrivateKey(plain);
                Array.Copy(enc, 0, r, i * blockSize, blockSize);
                log.DebugFormat("[Run {0}]: plain='{1}',enc='{2}'", i, Convert.ToBase64String(plain), Convert.ToBase64String(enc));
            }
            log.DebugFormat("Encrypted data: '{0}'", Convert.ToBase64String(r));
            return r;
        }
        public byte[] RSADecryptWithPrivateKeyLongData(byte[] data)
        {
            log.Debug("Begin long data RSA Decryption (PrivateKey)");
            log.DebugFormat("Data to decrypt: '{0}'", Convert.ToBase64String(data));
            
            int blockSize = (rsa.KeySize - 384) / 8;
            int blockNo = (int)Math.Ceiling((double)(data.Length / blockSize));
            log.DebugFormat("blockSize={0}, blockNo={1}", blockSize, blockNo);

            byte[] r = new byte[blockSize * blockNo];
            for (int i = 0; i < blockNo; ++i)
            {
                byte[] enc = new byte[blockSize];
                Array.Copy(data, i * blockSize, enc, 0, blockSize);
                byte[] plain = RSADecryptWithPrivateKey(enc);
                Array.Copy(plain, 0, r, i * blockSize, blockSize);
                log.DebugFormat("[Run {0}]: plain='{1}',enc='{2}'", i, Convert.ToBase64String(plain), Convert.ToBase64String(enc));
            }
            log.DebugFormat("Decrypted data: '{0}'", Convert.ToBase64String(r));
            return r;
        }
        public byte[] RSAEncryptWithPartnerKey(byte[] data)
        {
            log.Info("Encrypting with partner public key.");
            if (data == null)
                throw new ArgumentNullException("data");

            if (!isPartnerKeyLoaded)
                LoadPartnerPublicKey();

            if (data.Length > (partnerRsa.KeySize - 384) / 8 + 7)
            {
                log.Info("Data too long, will cut in blocks.");
                return RSAEncryptWithPartnerKeyLongData(data);
            }

            try
            {
                byte[] encrypted = partnerRsa.Encrypt(data, true);
                log.Info("Encrypted.");
                return encrypted;
            }
            catch (Exception e)
            {
                log.Error("Error while encrypting with partner assymetric key!", e);
                return null;
            }
        }

        public byte[] RSADecryptWithPrivateKey(byte[] data)
        {
            log.Info("Decrypting with private key.");
            if (data == null)
                throw new ArgumentNullException("data");

            if (!isPrivateKeyLoaded)
                LoadPrivateKey();

            if (data.Length > (rsa.KeySize - 384) / 8 + 7)
            {
                log.Info("Data too long, will cut in blocks.");
                return RSADecryptWithPrivateKeyLongData(data);
            }

            try
            {
                byte[] decrypted = rsa.Decrypt(data, true);
                log.Info("Decrypted.");
                return decrypted;
            }
            catch (Exception e)
            {
                log.Error("Error while decrypting with own assymetric key!", e);
                return null;
            }
        }

        public byte[] RSAEncryptWithPrivateKey(byte[] data)
        {
            log.Info("Encrypting with private key.");
            if (data == null)
                throw new ArgumentNullException("data");

            if (!isPrivateKeyLoaded)
                LoadPrivateKey();

            if (data.Length > (rsa.KeySize - 384) / 8 + 7)
            {
                log.Info("Data too long, will cut in blocks.");
                return RSAEncryptWithPrivateKeyLongData(data);
            }

            try
            {
                byte[] encrypted = rsa.Encrypt(data, true);
                log.Info("Encrypted.");
                return encrypted;
            }
            catch (Exception e)
            {
                log.Error("Error while encrypting with own assymetricKey", e);
                return null;
            }
        }

        public string RSAEncryptWithPartnerKey(string data)
        {
            return Convert.ToBase64String(
                              RSAEncryptWithPartnerKey(
                                 new UTF8Encoding().GetBytes(data)));
        }

        public string RSADecryptWithPrivateKey(string data)
        {
            byte[] decrypted = RSADecryptWithPrivateKey(Convert.FromBase64String(data));
            if (decrypted == null)
            {
                log.Error("Decryption returned null!");
                return null;
            }
            return new UTF8Encoding().GetString(decrypted);
        }

        public string RSAEncryptWithPrivateKey(string data)
        {
            return Convert.ToBase64String(
                                RSAEncryptWithPrivateKey(
                                    new UTF8Encoding().GetBytes(data)));
        }
        #endregion

        #region AES Keys
        private bool isSymmKeyRingLoaded { get { return symmKeys != null; } }
        private void LoadSymmetricKeys()
        {
            if (!isPrivateKeyLoaded)
                LoadPrivateKey();
            symmKeys = new SymmetricKeyRing("Cryptography", Properties.Settings.Default.SymmetricKeyFileName);
        }
        public string AESCurrentKey
        {
            get
            {
                if (Configuration.CurrentAESKeyID != String.Empty)
                    return Configuration.CurrentAESKeyID;
                else
                {
                    if (!isSymmKeyRingLoaded)
                        LoadSymmetricKeys();

                    string keyId = symmKeys.CreateNewKey();
                    //Message m = new Message(symmKeys[keyId].ToXMLString(true), MessageType.PARTNER_ADD_NEW_AES_KEY);
                    //Message r = MessageManager.Instance.SendMessage(m);
                    Message r;
                    try{
                        r = Entangled.Instance.AddAESKey(symmKeys[keyId].ToXMLString(true));
                    }catch(RequestException e){
                        log.Error("Caught RequestException while sending add aes key message.",e);
                        r = e.ErrorMessage;
                    }
                    if (r.Type == MessageType.AES_KEY_ADDED || r.Type == MessageType.MESSAGE_QUEUED_ON_SERVER)
                    {
                        Configuration.CurrentAESKeyID = keyId;
                        return keyId;
                    }
                    else
                    {
                        throw new EntanglementException("Cannot verify that partner got new key.");
                    }
                }
            }
        }
        public void AddNewAESKey(string keyXml)
        {
            if (!symmKeys.LoadKeyFromXML(keyXml))
                throw new CryptographicException("AES key was not loaded. Review logs for details.");
        }
        #endregion

        #region AES Encryption/Decryption
        public string AESEncrypt(string keyID, string data)
        {
            if (!isSymmKeyRingLoaded)
                LoadSymmetricKeys();

            RijndaelManaged aes = null;
            MemoryStream msEncrypt;
            try
            {
                aes = new RijndaelManaged();
                SymmetricKey key = symmKeys[keyID];
                if (key == null)
                {
                    log.Error("Key: " + keyID + " not found. Cannot encrypt.");
                    return null;
                }
                aes.IV = key.IV;
                aes.Key = key.Key;

                ICryptoTransform encryptor = aes.CreateEncryptor();
                msEncrypt = new MemoryStream();

                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                    {
                        swEncrypt.Write(data);
                    }
                }
            }
            catch (Exception e)
            {
                log.Error("Error while encrypting data with symmetric key: " + keyID, e);
                return null;
            }
            finally
            {
                if (aes != null)
                    aes.Clear();
            }
            return Convert.ToBase64String(msEncrypt.ToArray());
        }

        public string AESDecrypt(string keyID, string data)
        {

            if (!isSymmKeyRingLoaded)
                LoadSymmetricKeys();

            RijndaelManaged aes = null;
            string decrypted;
            try
            {
                aes = new RijndaelManaged();
                SymmetricKey key = symmKeys[keyID];
                if (key == null)
                {
                    log.Error("Key: " + keyID + " not found. Cannot decrypt.");
                    return null;
                }
                aes.IV = key.IV;
                aes.Key = key.Key;

                ICryptoTransform decryptor = aes.CreateDecryptor();
                using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(data)))
                {
                    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                        {
                            decrypted = srDecrypt.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception e)
            {
                log.Error("Error while decrypting data with symmetric key: " + keyID, e);
                return null;
            }
            finally
            {
                if (aes != null)
                    aes.Clear();
            }
            return decrypted;
        }

        public void EncryptXmlElement(XmlDocument xml, string elementName, string keyId)
        {
            if (!isSymmKeyRingLoaded)
                LoadSymmetricKeys();

            log.Info(String.Format("Encrypting XML element '{0}' with key '{1}'.", elementName, keyId));
            if (xml == null)
                throw new ArgumentNullException("xml");
            if (elementName == null || elementName == String.Empty)
                throw new ArgumentNullException("elementName");
            if (keyId == null || keyId == String.Empty)
                throw new ArgumentNullException("keyId");

            XmlElement xeToEncrypt = xml.GetElementsByTagName(elementName)[0] as XmlElement;
            if (xeToEncrypt == null)
                throw new XmlException("Element '" + elementName + "' was not found.");

            EncryptedXml eXml = new EncryptedXml();
            RijndaelManaged aes = new RijndaelManaged();
            SymmetricKey key = symmKeys[keyId];
            if (key == null)
                throw new CryptographicException("Key '" + keyId + "' was not found. Cannot encrypt XML element.");
            aes.IV = key.IV;
            aes.Key = key.Key;
            byte[] encryptedElement = eXml.EncryptData(xeToEncrypt, aes, false);
            EncryptedData edElement = new EncryptedData();
            edElement.Type = EncryptedXml.XmlEncElementUrl;

            string encryptionMethod;
            switch (aes.KeySize)
            {
                case 128:
                    encryptionMethod = EncryptedXml.XmlEncAES128Url;
                    break;
                case 192:
                    encryptionMethod = EncryptedXml.XmlEncAES192Url;
                    break;
                case 256:
                    encryptionMethod = EncryptedXml.XmlEncAES256Url;
                    break;
                default:
                    throw new CryptographicException("Unrecognised Rijndael KeySize:" + aes.KeySize);
            }

            edElement.EncryptionMethod = new EncryptionMethod(encryptionMethod);
            edElement.CipherData.CipherValue = encryptedElement;

            EncryptedXml.ReplaceElement(xeToEncrypt, edElement, false);
            log.Info("Encrypted.");
        }
        public void DecryptXmlElement(XmlDocument xml, string keyId)
        {
            if (!isSymmKeyRingLoaded)
                LoadSymmetricKeys();

            log.Info("Decrypting XML element with key:" + keyId);
            if (xml == null)
                throw new ArgumentNullException("xml");
            if (keyId == null || keyId == String.Empty)
                throw new ArgumentNullException("keyId");

            RijndaelManaged aes = new RijndaelManaged();
            SymmetricKey key = symmKeys[keyId];
            if (key == null)
                throw new CryptographicException("Key '" + keyId + "' was not found. Cannot encrypt XML element.");
            aes.IV = key.IV;
            aes.Key = key.Key;

            XmlElement encryptedElement = xml.GetElementsByTagName("EncryptedData")[0] as XmlElement;
            if (encryptedElement == null)
                throw new XmlException("The EncryptedData element was not found. Cannot decrypt XML element.");

            EncryptedData edElement = new EncryptedData();
            edElement.LoadXml(encryptedElement);

            EncryptedXml eXml = new EncryptedXml();

            byte[] decrypted = eXml.DecryptData(edElement, aes);
            eXml.ReplaceData(encryptedElement, decrypted);
            log.Info("Decrypted.");
        }
        #endregion


    }
}
